Skip to content

Conversation

vermont42
Copy link
Contributor

After the PR for SR-11580 merged, there was consensus, based on feedback by @xwu, that the new convenience methods floatingValue and integerValue, should be optional in order to prevent potential crashes. This PR implements that change.

This PR also implements new names, suggested by @xwu, for floatingValue and integerValue: floatLiteralValue and integerLiteralValue. These names LGTM.

@xwu also stated, "Ideally, [the convenience methods] would be generic over T: ExpressibleByIntegerLiteral and T: ExpressibleByFloatLiteral so users can get the value represented in any type they want." I am not a generics power user, but I am familiar with their use in the context of, for example, making a Node type generic over an Element type. I was unable to translate this understanding of the purpose and use of generics into an implementation of @xwu's suggestion. I would welcome a suggested new signature for floatLiteralValue or integerLiteralValue.

return potentialFloatingValue!
}

fileprivate var potentialFloatingValue: Double? {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One benefit of this PR is that this property is no longer needed.

XCTAssertNil(literalExpr)
XCTAssertNil(expectedValue)
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

git diff called out this absence of a terminating newline, so I added one.

@akyrtzi akyrtzi requested review from ahoppen and rintaro September 22, 2020 20:41
Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a quick scan of the PR, I’ve got two directions in which this could go:

1. The properties don’t need to be optional at all

AFAICT, the ExpressibleByFloatLiteral protocol has a requirement for an initialiser that takes a ExpressibleByFloatLiteral.FloatLiteralType. That type needs to conform to _ExpressibleByBuiltinFloatLiteral (see here). However, the only types that conform to _ExpressibleByBuiltinFloatLiteral according to here and here are Float16, Float, Double and Float80. So we cannot have a floating point literal that doesn’t fit in a Float80.

I know that this is a semantic property, but I would say that for the sake of usability, it would be alright to harness this semantic property. What do you think @xwu and @akyrtzi?

For Int the argument is similar with the only difference that we need to take into account signed and unsigned integers (Int and UInt being the biggest types). But I think we can tackle that problem by returning a type like the following from the intLiteralValue property and using either the signed or unsigned case depending on whether the literal starts with a - or not:

enum IntLiteralValue {
  case signed(Int)
  case unsigned(UInt)

  var signedValue: Int? { /* … */ }
  var unsignedValue: Int? { /* … */ }
}

2. We don’t need the must_uphold_invariant thing anymore

If we decide that we want to make the floatLiteralValue and intLiteralValue properties optional, we don’t need the entire must_uphold_invariant thing anymore.

In fact, as far as it’s implemented right now, there are currently two issues with the invariant:
a) It is no longer true that every syntactically valid literal upholds its invariant because it might not fit into the property’s type
b) We cannot set the contents of a float literal to anything that’s larger than the property’s type.

@akyrtzi
Copy link
Contributor

akyrtzi commented Sep 23, 2020

I know that this is a semantic property, but I would say that for the sake of usability, it would be alright to harness this semantic property. What do you think @xwu and @akyrtzi?

That would require that we change the parser to make overflowing literals syntactically invalid. But I don't think that is appropriate, I'd recommend that we adhere to what is valid syntax and avoid bringing in semantic/code-generation concepts.

@xwu
Copy link
Contributor

xwu commented Sep 24, 2020

It'll take me until the weekend to write up my feedback for this PR--sorry for the delay. Overall, it's going to be a hair more complicated than what's presented here.

@ahoppen Both _ExpressibleByBuiltinFloatLiteral and which types conform to it are internal implementation details (and unfortunate ones at that) of the standard library.

We can very much have a valid float literal that doesn't fit in a Float80; it leads to double-rounding error in today's Swift (see bugs SR-920, SR-3317, SR-3970, SR-7124, among others) and is why using float literals with Foundation.Decimal is a footgun. If necessary, using this implementation detail (i.e., bug) for an implementation here that could be kept in sync with the standard library should be fine, but using this implementation detail for the interface here is doubleplus ungood.

For integer literals, Int is not the widest built-in integer literal type (on 32-bit platforms, it is Int64); additionally, DoubleWidth could be brought back in the future, at which time there will be no limit to the widest built-in integer literal type. Meanwhile, the underlying representation of integer literals was long ago increased from 64 bits to 2048 bits, and then even that was superseded by an arbitrary-precision type in time for ABI stability; in other words, there truly is no limit to how big an integer literal can be.

The purpose of all of this work (including any future overhaul of ExpressibleByFloatLiteral to represent binary and decimal floating-point values losslessly with arbitrary precision) is precisely to make it so that float and integer literals are suitable for use with any floating-point or integer type, even third-party arbitrary precision ones. That is to say, in order to fulfill their semantic purpose, they must be able to represent values that are unrepresentable in any particular conforming type, or for that matter unrepresentable even in all available conforming types.

@ahoppen
Copy link
Member

ahoppen commented Sep 25, 2020

On second thought, I quite agree with @xwu and @akyrtzi that we shouldn’t rely on semantic functionality. AFAICT it should be sufficient if we make the properties optional and remove the must_uphold_invariant thing as outlined above.

I’m sorry for all the work you have put into this, @vermont42, only so it needs to be scrapped now…

@akyrtzi
Copy link
Contributor

akyrtzi commented Oct 3, 2020

@xwu would you be fine with making these simple changes for now (make the properties optional and remove must_uphold_invariant) so that at least we avoid potential crashes in the interface, and if you are able to outline in a forum post what do you recommend as an API I'd really appreciate it 🙏

Copy link
Contributor

@xwu xwu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay. Yes, I do have a comment on what an ideal API might be, but that shouldn't hold up this PR because it could be addressed further down the road.

However, I also have some comments on the correctness of the implementation, and I think those are pressing because they affect the raison d'être of this API: Users want to know, and this API purports to tell us, what value is represented by a particular literal expression. If it gives false positives and/or false negatives, then it does not serve that purpose, and it would not be a usable API to use.

As it happens, I think my comment on the API design is actually fairly straightforward to address, though not pressing, but my comments on the correctness of the implementation are quite hard to address but very much pressing. In fact, the potential crash is actually the least urgent in my view, as in that circumstance at least execution does not proceed with a misleading result; it's where the API returns an inaccurate result without crashing that really makes me worry. I really don't mind if we don't address the entirety of my comment on the API design, but if addressing the other issues prove to be unworkable in the short term, perhaps in the interim we could revert the previous PR?


fileprivate var potentialFloatingValue: Double? {
var floatLiteralValue: Double? {
let floatingDigitsWithoutUnderscores = floatingDigits.text.filter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless there is other validation of text of which I'm unaware, it's not sufficient simply to drop all underscores. There are rules surrounding where underscores may appear, and there are combinations of underscores that are invalid. For example, 1.0e_3 is not permitted, because an underscore in the floating point exponent is not allowed. Simply dropping underscores means that you will have false positives where floatLiteralValue returns a non-nil result but in fact the literal represented is invalid.

The rules for where underscores are allowed (where, how many consecutively, etc.) aren't easily found and, even when found, may be inaccurate. For instance, according to PEP 515 (which added underscore separators to numeric literals in Python and surveyed the rules in other languages), Swift documentation says that any number of underscores are allowed between digits, but in fact they are allowed in Swift at the end of literals after the last digit as well.

This will likely require either experimentation or a careful reading of the Swift compiler's code. If you undertake this work, please do document it somewhere for posterity.

Similar issues may apply to integer literals too, although to a lesser degree because the syntax is less complex. Even if there turn out to be no such issues, it will be good to undertake the exercise of proving that and documenting it.

let floatingDigitsWithoutUnderscores = floatingDigits.text.filter {
$0 != "_"
}
return Double(floatingDigitsWithoutUnderscores)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the String-to-Double conversion API may produce both false positives and false negatives (again, unless there is other validation of text of which I'm unaware); in other words, there will be valid literals for which this function will return nil, and invalid literals for which this function returns a value. The reason for this is that Swift's rules for numeric literals diverge from what's allowed for C's numeric literals, while conversions to and from String values use (or at least follow more closely) C's rules, and it's too late to change that behavior because it will break existing code.

As an example, .5 is not a valid Swift float literal, but ".5" is a string that can be converted to a floating-point value using Double(".5"). The reason for this particular divergence is that Swift has a leading-dot member syntax.

There are other examples listed here that will likely require some experimentation to ensure are still applicable and as described. It is likely that I have missed some of the differences too (for instance, I never did list the differences in behavior when a float literal is too large to be represented by the desired type versus a string).

Again, similar issues may apply to integer literals, and even if there turn out to be no such issues, it would be important to prove and document that.

var integerValue: Int {
return potentialIntegerValue!
var isValid: Bool {
floatLiteralValue != nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest removal of isValid altogether for now; whether a float literal is a valid or invalid literal has no relationship to whether it can be converted to a value of any particular type, for reasons we discussed earlier.

It may be, however, that in your experimentation regarding the issues above, you end up having a sufficiently complete implementation validating Swift's syntactic rules surrounding literals that you can implement isValid without reference to conversion to any particular floating-point type. In that case, then by all means include an isValid member that checks for validity of the literal.

The same comment applies to the other isValid for integer literals.


fileprivate var potentialIntegerValue: Int? {
public extension IntegerLiteralExprSyntax {
var integerLiteralValue: Int? {
Copy link
Contributor

@xwu xwu Oct 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so here's my one comment on API design. I think users will want to know what value is represented by their numeric literal expression for types other than the default Int and Double. It would be very reasonable to ask what's the UInt32 value represented by a certain literal, for instance.

Therefore, we can use a (generic) method instead of a property to allow the user to pass a variable indicating the desired type:

func integerLiteralValue<T>(ofType _: T.Type) -> T
where T: ExpressibleByIntegerLiteral { ... }

Users could then write: literalExpr.integerLiteralValue(ofType: UInt32.self). Now, it's likely that users will most often want a value of the default literal types, so it's reasonable to add that default so that users don't have to type it out every time:

func integerLiteralValue<T>(ofType _: T.Type = IntegerLiteralType) -> T
where T: ExpressibleByIntegerLiteral { ... }

Now, the use site simply differs from what you have currently in this PR by a pair of parentheses. Pretty good.

However, if you were to try to implement this API, you'd find that you actually need to constrain T a bit further in order to use the String conversion APIs. It's tempting to write where T: ExpressibleByIntegerLiteral, T: LosslessStringConvertible, but in fact this raises a problem: there's no semantic guarantee that the string format from which a type can be losslessly converted is the same as the literal format (and in fact, they don't, as described above, but at least it's somewhat close for the built-in types). But in practice, we might be able to get away with it.

However, you'd run into another problem when it comes to integer literals, because if you want to pass the radix argument, you'd need to constrain to T: FixedWidthInteger instead of T: LosslessStringConvertible. This is fine, but floating-point types are expressible by integer literals (this is another way in which literals and numeric types differ from their C counterparts) but they are not fixed-width integer types; it'd be difficult to make it possible to convert from binary or octal integer literals to floating-point types without implementing your own conversions from scratch here. This is certainly not necessary for this PR and likely out of scope for this project entirely, but if we ignore floating-point types and integer literals, you can get 80% of the way just by making a few adjustments to the API design.

testFloatValue(text: "0xaffab1e.e1fP-2", expectedValue: 0xaffab1e.e1fP-2)
testFloatValue(text: "🥥", expectedValue: nil)
// The following results in a valid FloatLiteralExprSyntax, but because Double.greatestFiniteMagnitude
// is 1.7976931348623157e+308, floatLiteralValue should be nil.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should not be nil. If you write let x = 1.7976931348623157e+309, you get an actual Double which is infinity. This is one of those divergences I discuss above between the string conversion APIs and the float literal APIs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last commit was about removing must_uphold_invariant and verifying that unit tests still pass. I have not yet explored additional changes.

@shahmishal shahmishal closed this Oct 6, 2020
@shahmishal
Copy link
Member

The Swift project moved the default branch to main and deleted master branch, so GitHub automatically closed the PR. Please re-create the pull request with main branch.

More detail about the branch update - https://forums.swift.org/t/updating-branch-names/40412

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants